π PR - https://github.com/grpc/grpc-go/pull/8375
1. PRμ λͺ©μ
κΈ°μ‘΄μ μμ±λμ§ μμλ ExitIdle κ΄λ ¨ λ©μλμ λ¨μ ν μ€νΈλ₯Ό μμ±νμ¬ ν μ€νΈ 컀λ²λ¦¬λ₯Ό μΆκ°νλ PR μ λλ€. μ¬κΈ°μ ExitIdle μ΄ λ¬΄μμ μλ―Ένλμ§ μλμμ μ€λͺ νκ² μ΅λλ€. μ΄λ² PR μμλ BalancerGroup μ ExitIdle μ΄ λ€μν μλ리μ€μμ μ¬λ°λ₯΄κ² μ²λ¦¬λλμ§ κ²μ¦νκΈ° μν ν μ€νΈ μ½λλ₯Ό μΆκ°ν©λλ€.
1-1. ExitIdle μ΄λ?
ExitIdleμ gRPC ν΄λΌμ΄μΈνΈκ° μ ν΄(idle) μνμμ λ²μ΄λ(exit) λ€μ μ°κ²°μ νμ±ννλ λ©μ»€λμ¦μ μλ―Έν©λλ€. gRPC ν΄λΌμ΄μΈνΈλ μλ²μμ μ°κ²°μ μ μ§νμ§λ§, μΌμ μκ° λμ μμ²μ΄ μμΌλ©΄ 리μμ€λ₯Ό μ μ½νκΈ° μν΄ μ°κ²°μ΄ μ ν΄ μνλ‘ μ νλ©λλ€. μ ν΄ μνμμλ μλ²μμ μ°κ²°μ΄ λκΈ°κ±°λ, μμ²μ΄ μμ λ μ¦μ μ²λ¦¬ν μ μλ μνκ° λ μ μμ΅λλ€. ExitIdleμ μλ‘μ΄ μμ²μ΄ λ€μ΄μμ λ, ν΄λΌμ΄μΈνΈκ° μ ν΄ μνμμ λ²μ΄λ μλ²μμ μ°κ²°μ μ¬κ°νκ³ , μ μμ μΈ μμ² μ²λ¦¬ μνλ‘ λμκ°λλ‘ νΈλ¦¬κ±°ν©λλ€.
subConn
μ gRPC client μ νλμ νΉμ μλ² κ°μ λ¨μΌ μ°κ²°μ μλ―Ένλ κ°λ
μΌλ‘, IDLE μνμ SubConn μ subConn.Connectκ° νΈμΆλ λκΉμ§ μλμΌλ‘ μ¬μ°κ²°νμ§ μμ΅λλ€.
gRPC μ default idleTimeout μ 30λΆμΌλ‘ μ±λμ μ§ν μ€μΈ RPCκ° μκ³ μλ‘μ΄ RPCκ° μμλμ§ μμΌλ©΄ μ±λμ idle λͺ¨λλ‘ μ νλ©λλ€.
μ΄λ name resolverμ load balancerκ° μ’
λ£λ©λλ€.
λ°λλ‘ ExitIdle μ΄ νΈλ¦¬κ±° λλ©΄ name resolver μ load balancer μμ μ¬μμ λ©λλ€.
func defaultDialOptions() dialOptions { return dialOptions{ copts: transport.ConnectOptions{ ReadBufferSize: defaultReadBufSize, WriteBufferSize: defaultWriteBufSize, UserAgent: grpcUA, BufferPool: mem.DefaultBufferPool(), }, bs: internalbackoff.DefaultExponential, idleTimeout: 30 * time.Minute, defaultScheme: "dns", maxCallAttempts: defaultMaxCallAttempts, useProxy: true, enableLocalDNSResolution: false, } }
gRPC μ channel μ΄λ
`μ±λ(channel)` μ ν΄λΌμ΄μΈνΈκ° μλ²μ RPC(remote procedure call) λ₯Ό λ³΄λΌ μ μλ ν΅μ ν΅λ‘ μν μ νλ λ Όλ¦¬μ μ°κ²°μ μλ―Έν©λλ€. μ¬κΈ°μ μ±λμ λ¨μν νλμ tcp μ°κ²°μ μλ―Ένλ κ²μ΄ μλ, μ¬λ¬ κΈ°λ₯μ μΆμννκ³ κ΄λ¦¬νλ μμ κ°λ μΌλ‘ μλμ μν μ μνν μ μμ΅λλ€.
- μ°κ²° κ΄λ¦¬ : μ±λμ ν κ° μ΄μμ 물리μ μΈ TCP μ°κ²°(subConn) μ κ΄λ¦¬
- λΆν λΆμ° : μ¬λ¬ μλ² μΈμ€ν΄μ€μ λΆν λΆμ°μ μ²λ¦¬
- μ°κ²° μ¬μλ: μ°κ²°μ΄ λμ΄μ‘μ λ μλ μ¬μ°κ²° μλ.
type ExitIdler interface { // ExitIdleμ LB μ μ± μκ² λ°±μλμ μ¬μ°κ²°νκ±°λ // IDLE μνμμ λ²μ΄λλλ‘ μ§μν©λλ€ ExitIdle() }
μ 리νμλ©΄ ExitIdle μ μ ν΄ μνμΈ ν΄λΌμ΄μΈνΈκ° νμ±νλμ΄ μ¬μμ²μ 보λ΄λ νΈλ¦¬κ±° μν μ μ 곡ν©λλ€.
1-2. BalancerGroup μ΄λ?
balancergroup
μ gRPC ν΄λΌμ΄μΈνΈ μΈ‘μμ μ¬λ¬ λ°±μλ μ°κ²°μ κ΄λ¦¬νκ³ μμ²μ μ μ ν λ°±μλλ‘ λΆμ°νκΈ° μν λΆν λΆμ° κ΅¬μ± μμμ
λλ€.
νλμ gRPC μλΉμ€λ μ¬λ¬ μλ²(λ°±μλ)λ‘ κ΅¬μ±λ μ μμΌλ©°, balancergroup μ μ΄λ¬ν μλ²λ₯Ό κ·Έλ£Ήννμ¬ ν¨μ¨μ μΈ λΆν λΆμ°μ κ°λ₯μΌ ν©λλ€.
ν΄λΌμ΄μΈνΈμ ν΅μ ν μ¬λ¬ μλ² μ€μμ νμ¬ μμ²μ λ³΄λΌ μ΅μ μ μλ²λ₯Ό μ ννλ λ‘μ§μ ꡬννλ©°, μ΄λ round robin,least-connection κ°μ λ€μν λΆν λΆμ° μ λ΅μ μ νν μ μμ΅λλ€.
balancergroup μ ν΄λΌμ΄μΈνΈ-μλ² κ° μ¬λ¬ κ°μ 물리μ μ°κ²°μ λ
Όλ¦¬μ μΈ νλμ λ¨μλ‘ κ΄λ¦¬νμ¬, λ‘λ λ°Έλ°μ± μ μ±
μ΄ λ³΅μ‘ν μ°κ²° μνλ₯Ό κ°λ°μκ° μ§μ λ€λ£¨μ§ μκ³ λ μΆμνλ λ°±μλ κ·Έλ£Ήμ λν κ²°μ μ λ΄λ¦΄ μ μλ μν μ ν©λλ€.
ExitIdle
κ³Ό BalancerGroup
λ ν΄λΌμ΄μΈνΈ μΈ‘ λ‘λ λ°Έλ°μ±μμ ν¨κ» λμν©λλ€. μ ν΄ μνμ ν΄λΌμ΄μΈνΈκ° νμ±νλμ΄ μμ²μ 보λ΄κΈ° μν νΈλ¦¬κ±° μν μ ExitIdle μ΄ μ 곡νκ³ ,
BalancerGroup μ νμ±νλ ν΄λΌμ΄μΈνΈκ° μμ²μ λ³΄λΌ λ μ΅μ μ μλ²λ₯Ό μ νν μλλ‘ λΆν λΆμ° μμ
μ μ€μ μν μ μνν©λλ€.
2. PR - ν μ€νΈ μΌμ΄μ€ μμ±
BalancerGroup κ΄λ ¨ ν μ€νΈ μμ±:
2-1. TestBalancerGroup_UpdateClientConnState_AfterClose
BalancerGroupμ΄ λ«ν(Close) ν UpdateClientConnState λ©μλκ° νΈμΆλμ λμ λμμ κ²μ¦νλ ν μ€νΈλ‘, BalancerGroup close νμλ λ μ΄μ νμ balancer μκ² μν μ λ°μ΄νΈ μ νκ° λμ§ μλλ‘ κ²μ¦ν©λλ€.
func (s) TestBalancerGroup_UpdateClientConnState_AfterClose(t *testing.T) { balancerName := t.Name() clientConnStateCh := make(chan struct{}, 1) // stub balancer λ±λ‘ stub.Register(balancerName, stub.BalancerFuncs{ // UpdateClientConnState νΈμΆλ λ λ§λ€ μ±λμ μ νΈλ₯Ό 보λ΄λλ‘ ν¨ UpdateClientConnState: func(_ *stub.BalancerData, _ balancer.ClientConnState) error { clientConnStateCh <- struct{}{} return nil }, }) bg := New(Options{ CC: testutils.NewBalancerClientConn(t), // ν μ€νΈμ© ν΄λΌμ΄μΈνΈ μ°κ²° μμ± BuildOpts: balancer.BuildOptions{}, StateAggregator: nil, Logger: nil, }) bg.Add(testBalancerIDs[0], balancer.Get(balancerName)) bg.Close() // BalancerGroup μ μ’ λ£ μνλ‘ λ³κ²½ -> bg μ μνλ balancer λ μ ν΄ μνκ° λμ΄μΌ ν¨ if err := bg.UpdateClientConnState(testBalancerIDs[0], balancer.ClientConnState{}); err != nil { t.Fatalf("Expected nil error, got %v", err) } select { case <-clientConnStateCh: t.Fatalf("UpdateClientConnState was called after BalancerGroup was closed") case <-time.After(defaultTestShortTimeout): } }
UpdateClientConnState λ₯Ό νΈμΆνλ©΄ clientConnStateCh μ±λμ μ νΈκ° μ€λλ‘ stub μ λ±λ‘νκ³ , select λ¬Έμ μ¬μ©νμ¬ ν΄λΉ μ±λμ μ νΈκ° μ€λμ§ νΉμ defaultTestShortTimeout μ΄ μ§λλ©΄ μ μμ’ λ£ λλλ‘ λκ°μ§ μΌμ΄μ€λ₯Ό κ²μ¦ν©λλ€. μ’ λ£λ BalancerGroup μμ UpdateClientConnState λ₯Ό νΈμΆν μ μλ κ²μ΄ μ μ λμμ΄κΈ° λλ¬Έμ clientConnStateCh κ° μ νΈλ₯Ό λ°μΌλ©΄ ν μ€νΈλ μ€ν¨ν©λλ€. μ€μ λ‘ UpdateClientConnState ꡬνμ 보면 balancer group μ΄ λ«ν νμ nil μ 리ν΄νμ¬ graceful νκ² μ²λ¦¬λλ κ²μ νμΈν μ μμ΅λλ€.
// UpdateClientConnState handles ClientState (including balancer config and // addresses) from resolver. It finds the balancer and forwards the update. func (bg *BalancerGroup) UpdateClientConnState(id string, s balancer.ClientConnState) error { bg.outgoingMu.Lock() defer bg.outgoingMu.Unlock() if bg.outgoingClosed { return nil } if config, ok := bg.idToBalancerConfig[id]; ok { return config.updateClientConnState(s) } return nil }
2-2. TestBalancerGroup_ResolverError_AfterClose
BalancerGroupμ΄ λ«ν ν ResolverError λ©μλκ° νΈμΆ μ λμμ κ²μ¦νλ ν
μ€νΈλ‘
BalancerGroup close νμλ λ μ΄μ νμ balancer μκ² resolver μλ¬κ° μ νλμ§ μλλ‘ κ²μ¦ν©λλ€.
ResolverErrorλ
gRPCμ name resolution κ³Όμ μμ λ°μνλ μλ¬λ₯Ό μ²λ¦¬νλ λ©μλμ
λλ€.
DNS μ‘°ν μ€ν¨, Service Discovery μ₯μ , λ€νΈμν¬ μ°κ²° λ¬Έμ λ±μ μν©μμ name resolverκ° μλΉμ€ μ΄λ¦μ μ€μ μλ² μ£Όμλ‘ λ³ννμ§ λͺ»ν λ νΈμΆλ©λλ€.
ResolverErrorλ₯Ό νΈμΆνλ©΄ resolveErrorCh μ±λμ μ νΈκ° μ€λλ‘ stubμ λ±λ‘νκ³
select λ¬Έμ μ¬μ©νμ¬ ν΄λΉ μ±λμ μ νΈκ° μ€λμ§ νΉμ defaultTestShortTimeoutμ΄ μ§λλ©΄ μ μμ’
λ£ λλλ‘ λκ°μ§ μΌμ΄μ€λ₯Ό κ²μ¦ν©λλ€.
μ’
λ£λ BalancerGroup μμ ResolverErrorκ° νμ balancerμ μ νλμ§ μλ κ²μ΄ μ μ λμμ΄κΈ° λλ¬Έμ resolveErrorChκ° μ νΈλ₯Ό λ°μΌλ©΄ ν
μ€νΈλ μ€ν¨ν©λλ€.
func (s) TestBalancerGroup_ResolverError_AfterClose(t *testing.T) { balancerName := t.Name() resolveErrorCh := make(chan struct{}, 1) stub.Register(balancerName, stub.BalancerFuncs{ ResolverError: func(_ *stub.BalancerData, _ error) { resolveErrorCh <- struct{}{} }, }) bg := New(Options{ CC: testutils.NewBalancerClientConn(t), BuildOpts: balancer.BuildOptions{}, StateAggregator: nil, Logger: nil, }) bg.Add(testBalancerIDs[0], balancer.Get(balancerName)) bg.Close() bg.ResolverError(errors.New("test error")) select { case <-resolveErrorCh: t.Fatalf("ResolverError was called on sub-balancer after BalancerGroup was closed") case <-time.After(defaultTestShortTimeout): } }
ResolverError ꡬνμμ balancer group μ΄ λ«ν νμ early return μΌλ‘ graceful ν μ²λ¦¬νλ κ²μ νμΈν μ μμ΅λλ€.
// ResolverError handles name resolution errors from the name resolver. func (bg *BalancerGroup) ResolverError(err error) { bg.outgoingMu.Lock() defer bg.outgoingMu.Unlock() if bg.outgoingClosed { return } for _, config := range bg.idToBalancerConfig { config.resolverError(err) } }
2-3. TestBalancerGroup_ExitIdle_AfterClose
BalancerGroupμ΄ λ«ν ν ExitIdle λ©μλκ° νΈμΆλμ λμ λμμ κ²μ¦νλ ν
μ€νΈ μ
λλ€.
BalancerGroup close νμλ λ μ΄μ νμ balancer μκ² ExitIdle νΈμΆμ΄ μ νλμ§ μλλ‘ κ²μ¦ν©λλ€.
ExitIdle μ κ°λ μ νμ νλ©΄ μ¬μ€ λ무λ λΉμ°ν κ²μ¦ μ½λμ λλ€. ExitIdleμ μ ν΄ μνμ ν΄λΌμ΄μΈνΈλ₯Ό νμ±ννμ¬ μλ²μμ μ°κ²°μ μ¬κ°νλ κ²μ΄ ν΅μ¬ λ©μ»€λμ¦μΌλ‘, μΌλ°μ μΌλ‘λ μλ‘μ΄ RPC μμ²μ΄ λ€μ΄μ€κ±°λ λͺ μμ μΌλ‘ Connect() λ©μλκ° νΈμΆλ λ νΈλ¦¬κ±°λμ΄ IDLE μνμ SubConn λ€μ΄ μ°κ²°μ μλν©λλ€.
func (s) TestBalancerGroup_ExitIdle_AfterClose(t *testing.T) { balancerName := t.Name() exitIdleCh := make(chan struct{}, 1) stub.Register(balancerName, stub.BalancerFuncs{ ExitIdle: func(_ *stub.BalancerData) { exitIdleCh <- struct{}{} }, }) bg := New(Options{ CC: testutils.NewBalancerClientConn(t), BuildOpts: balancer.BuildOptions{}, StateAggregator: nil, Logger: nil, }) bg.Add(testBalancerIDs[0], balancer.Get(balancerName)) bg.Close() bg.ExitIdle() select { case <-exitIdleCh: t.Fatalf("ExitIdle was called on sub-balancer even after BalancerGroup was closed") case <-time.After(defaultTestShortTimeout): } }
ExitIdleμ νΈμΆνλ©΄ exitIdleCh μ±λμ μ νΈκ° μ€λλ‘ stubμ λ±λ‘νκ³ , select λ¬Έμ μ¬μ©νμ¬ ν΄λΉ μ±λμ μ νΈκ° μ€λμ§ νΉμ defaultTestShortTimeoutμ΄ μ§λλ©΄ μ μμ’ λ£ λλλ‘ λκ°μ§ μΌμ΄μ€λ₯Ό κ²μ¦ν©λλ€. μ’ λ£λ BalancerGroupμμ ExitIdleμ΄ νμ balancerμ μ νλμ§ μλ κ²μ΄ μ μ λμμ΄κΈ° λλ¬Έμ exitIdleChκ° μ νΈλ₯Ό λ°μΌλ©΄ ν μ€νΈλ μ€ν¨ν©λλ€.
μ€μ λ‘ ExitIdle ꡬνμ 보면 balancer groupμ΄ λ«ν νμ early return ν graceful νκ² μ²λ¦¬λλ κ²μ νμΈν μ μμ΅λλ€.
// ExitIdle starts the balancing picker for use and exits idle mode. func (bg *BalancerGroup) ExitIdle() { bg.outgoingMu.Lock() defer bg.outgoingMu.Unlock() if bg.outgoingClosed { return } for _, config := range bg.idToBalancerConfig { config.exitIdle() } }
μ 3κ°μ ν μ€νΈμ½λμ μ μ¬ν λ©μ»€λμ¦μΌλ‘ λλ¨Έμ§ μν©μ λν΄μλ ν μ€νΈ μ½λλ₯Ό μμ±νκ³ PR μ λ§λ¬΄λ¦¬ ν μ μμμ΅λλ€.
3. λ§λ¬΄λ¦¬
grpc-go λ΄λΆ λ©μ»€λμ¦ μ€ BalancerGroup κ³Ό ExitIdle μ λμμ리λ₯Ό μ΄ν΄νκ³ λ€μν μλ리μ€μ λν ν
μ€νΈ μ½λλ₯Ό μμ±ν μ μλ PR μ΄μμ΅λλ€.
리뷰λ₯Ό λ°κΈ° μ channel λμ variable μ μ¬μ©ν΄ κ²μ¦ μ½λλ₯Ό μμ±νμ λ λ€μκ³Ό κ°μ 리뷰λ₯Ό λ°κ² λμ΅λλ€.
κ°λ¨ν λ§νλ©΄ λ³μ λμ μ±λμ μ¬μ©νλ λ§μΈλ°, grpc μ²λΌ λ΄λΆμ μΌλ‘ λ§μ κ³ λ£¨ν΄μ μ¬μ©νλ λΌμ΄λΈλ¬λ¦¬λ₯Ό ν
μ€νΈν λ race condition μ λν κ³ λ €λ
νμνκΈ° λλ¬Έμ λ΄λΆμ μΌλ‘ λκΈ°νκ° λ³΄μ₯λλ channel μ μ¬μ©ν΄μΌ νλ€λ κ²λ μ μ μμμ΅λλ€.